﻿###########################################
#
#  Super NeatBoy
#
#  A platformer game engine example for Octo
#
#  'oh no; what has happened to all my leaves?
#   I must find them and bring them back home!'
#
#  Move with A/D or arrows,
#  Jump with E or space.
#
#  This program uses XO-Chip instructions,
#  and expects to be run at around 500-1000
#  cycles/frame. The plane 2 and blend
#  colors should be identical to make
#  the player sprite opaque. Additionally,
#  the buzzer color should probably be set
#  to match the background.
#  The "clip sprites at screen edges instead of wrapping"
#  compatibility flag will hide a minor aesthetic issue
#  when the player moves between screens horizontally.
#
###########################################

# v0-v4, vf reserved as temporaries.
# v5-v6 are still available as globals.

:alias stemp   v7 # temp register for sound effects
:alias time    v8 # global frame counter for portal animation
:alias px      v9 # player x position, in screen pixels
:alias py      vA # player y position, in screen pixels
:alias px_sign vB # {-1, 0, 1} on x axis
:alias dx      vC # velocity on x axis, in pixels
:alias frame   vD # player animation frame
:alias flags   vE # movement status flags (see below)

:const FLAG_LEFT     1 # is the player facing left? (otherwise, face right)
:const FLAG_JUMPED   2 # has the player already jumped since last touching ground?
:const FLAG_PRESSED  4 # is the player still holding down jump since last touching ground?
:const FLAG_FELL     8 # did the player fall off a ledge, rather than jumping?

# persistent state which doesn't need
# to reside in a global register:

: player-room     0 # room index
: player-entrance 3 # last entrance direction
: leaf-count      0 # for convenience, a total

# note: storing the above variables in global
# registers would be an easy way to save code space,
# if they aren't needed for any other purpose...

:const ROOM_COUNT 16

: leaf-found
	# collectibles for victory condition,
	# stored as flags by room index.
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

: total-deaths    0
: total-time-frac 0 # 1/60ths of a second (0-59)
: total-time-sec  0 # full seconds        (0-59)
: total-time-min  0 # full minutes

:macro world-reset {
	i := player-room
	v0 := 0 # room
	v1 := 3 # entrance
	v2 := 0 # leaves
	save v2

	i := leaf-found
	v0 := 0
	v1 := 0
	loop
		save v0
		v1 += 1
		:calc ZERO_INIT { ROOM_COUNT + 4 } # clear deaths/time too
		if v1 != ZERO_INIT then
	again
}

# note: changing this goal will require tweaking leaf-progress:
:const LEAF_COUNT 8

:const ENTER_RIGHT 0
:const ENTER_LEFT  1
:const ENTER_UP    2
:const ENTER_DOWN  3

# every room has up to four distinct player respawn
# locations used whenever the player dies. (see { A, B, C, D })
# the spawn point chosen is based on the direction
# from which the player entered the room.
# the X/Y spawn coordinates for the current room are stored here:
: room-spawns
	0 0 # RIGHT
	0 0 # LEFT
	0 0 # UP
	0 0 # DOWN

# to reduce flicker, this game always tracks the player's
# location and animation frame after each update in this buffer.
# this information can then be used to quickly erase the player sprite
# immediately before redrawing:
: player-prev
	0 # anim offset
	0 # x
	0 # y

# when a player jumps, they leave behind a temporary
# dust cloud animation:
: player-dust
	0 # timer (see DUST_END)
	0 # x
	0 # y

###########################################
#
#  Utility Macros
#
###########################################

# convenience macros for manipulating the flag register:
:macro set-flag FLAG {
	vf := FLAG
	flags |= vf
}
:macro clear-flag FLAG {
	:calc unflag { ~ FLAG }
	vf := unflag
	flags &= vf
}
:macro if-flag FLAG {
	vf := FLAG
	vf &= flags
	if vf != 0
}
:macro if-not-flag FLAG {
	vf := FLAG
	vf &= flags
	if vf == 0
}

# together, these two macros can be used to create a horizontally-mirrored
# copy of an existing 8x8 sprite, like the player sprites:
:macro mirror-row {
	:calc t { @ sprite-base }
	:byte { (   1 & t >> 7 ) | (  2 & t >> 5 ) | (  4 & t >> 3 ) | (  8 & t >> 1 ) |
	        ( 128 & t << 7 ) | ( 64 & t << 5 ) | ( 32 & t << 3 ) | ( 16 & t << 1 ) }
	:calc sprite-base { 1 + sprite-base }
}
:macro mirror-frame BASE {
	:calc sprite-base { player-frames + BASE }
	mirror-row  mirror-row  mirror-row  mirror-row
	mirror-row  mirror-row  mirror-row  mirror-row
}

# 'wait' will wait for at *least* TIME to elapse.
# 'sync' will wait for at *most* TIME to elapse.
:macro wait TIME {
	vf := TIME delay := vf
	loop vf := delay if vf != 0 then again
}
:macro sync TIME {
	loop vf := delay if vf != 0 then again
	vf := TIME delay := vf
}

# helpers for multiple memory indirection/pointers:
# unpack16 places a computed 16-bit address in v registers,
# pointer writes a computed 16-bit address into memory as a literal,
# and indirect creates an i := long NNNN instruction with a label
# halfway through, ready to be overwritten with a new 16-bit value:
:macro unpack16 ADDR {
	:calc hi { 0xFF & ADDR >> 8 }  v0 := hi
	:calc lo { 0xFF & ADDR      }  v1 := lo
}
:macro pointer ADDR {
	:byte { 0xFF & ADDR >> 8 }
	:byte { 0xFF & ADDR      }
}
:macro indirect LABEL {
	0xF0 0x00 : LABEL 0x00 0x00 # i := long NNNN
}

# keep a value within boundaries
:macro clamp REG MIN MINV MAX MAXV {
	if REG >= MIN begin
		REG := MINV
	else
		if REG >= MAX then REG := MAXV
	end
}

# helpers for working with XO-Chip high RAM.
# the 'to-data' macro indicates that the ensuing data
# should be assembled into high RAM (which must be indexed
# with the i := long NNNN instruction),
# and the 'to-code' macro swaps back to assembling in low RAM.
# using these macros allows you to declare data before using it,
# while keeping data out of precious low RAM:
:calc CODE_POS { 0x200  }
:calc DATA_POS { 0x1000 }
:macro to-code { :calc DATA_POS { HERE }  :org { CODE_POS } }
:macro to-data { :calc CODE_POS { HERE }  :org { DATA_POS } }

###########################################
#
#  BATS!
#
###########################################

to-data

: bat-sprites
	0x5C 0xFE 0xFF 0x71 0x58 0x00 0x00 0x00
	0x00 0x3C 0xFE 0xFE 0x5F 0x00 0x00 0x00
	0x00 0x30 0xF8 0xF8 0x7A 0x5E 0x2C 0x00
	0x00 0x3C 0xFE 0xFE 0x5E 0x00 0x00 0x00

: bat-wobble
	0 -1 -2 -1 0 1 2 1

to-code

: bat-stash    0   # flag
: bat-stash-x  0 0 # x, y

: bat-draw
	i := bat-stash
	load v2
	if v0 == 0 then return
	plane 2

	# slow vertical wobble
	v0 := 0b11100
	v0 &= v1 # x
	v0 >>= v0
	v0 >>= v0
	i := long bat-wobble
	i += v0
	load v0
	v2 += v0 # final y

	# frame cycle
	i := long bat-sprites
	v0 := 0b110
	v0 &= v1 # x
	v0 += v0 # * 2
	v0 += v0 # * 4
	i  += v0

	sprite v1 v2 8
;

:macro bat-clear {
	v0 := 0
	i := bat-stash
	save v0
}
:macro bat-init {
	v0 := -1
	i := bat-stash
	save v2
}
:macro bat-update {
	bat-draw
	v1 += -2
	i := bat-stash-x
	save v1 - v1
	bat-draw
}

###########################################
#
#  Leaves
#
###########################################

to-data

: leaf-sprite
	0xC0 0xB4 0x5D 0x75 0x2F 0x7A 0x0E 0x39
: out-of-8
	0x2F 0x49 0x4F 0x49 0x8F

to-code

:const STATUS_X 1
:const STATUS_Y 122

: leaf-stash
	0 # x
	0 # y

: leaf-draw
	i := leaf-stash
	load v1
	if v1 == -1 then return
	plane 2
	i := long leaf-sprite
	sprite v0 v1 8
;

: leaf-clear
	v0 := -1
	v1 := -1
	i  := leaf-stash
	save v1
;

: leaf-progress
	i := leaf-count
	load v0
	i := hex v0
	v1 := STATUS_X
	v2 := STATUS_Y
	sprite v1 v2 5
	v1 += 5
	i := long out-of-8
	sprite v1 v2 5
;

:macro leaf-init {
	# don't initialize if the leaf in this
	# room has already been taken:
	i := player-room
	load v0
	i := long leaf-found
	i += v0
	load v0
	if v0 == 0 begin
		i := leaf-stash
		save v1 - v2
	end
}
:macro leaf-exists {
	# v0 != -1 iff there's a leaf here
	i := leaf-stash
	load v0
}
:macro leaf-get {
	sfx sound-get

	# flicker leaf
	v2 := 9
	loop
		leaf-draw
		wait 3

		v2 += -1
		if v2 != 0 then
	again
	leaf-clear

	# record pickup
	i := player-room
	load v0
	i := long leaf-found
	i += v0
	v0 := 1
	save v0
	i := long leaf-count
	load v0 - v0
	v0 += 1
	save v0 - v0

	# animate progress counter
	leaf-progress
	wait 60
	leaf-progress
}

###########################################
#
#  Full-Screen Graphics
#
###########################################

to-data

# These screens were prepared using EZ-Pack:
# http://beyondloom.com/tools/ezpack.html
# starting with a 128x64 pixel black-and-white image,
# use default settings aside from changing
# "Sprite Size" to 0 for 16x16 sprites in each chunk.

: title-data
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x00 0x01 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x00 0x01 0x00 0x01 0x00 0x00 0x00 0x00
	0x00 0x00 0x03 0xE7 0x03 0xF7 0x03 0x36 0x03 0x37 0x03 0x36 0x03 0x37 0x03 0x37
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x70 0x07 0xF8 0x07 0xDB 0x06 0x9B 0x36 0x83 0x36 0xC3 0x36
	0xC3 0x36 0x63 0x36 0x73 0x36 0x3B 0x37 0x3B 0x37 0x1B 0x36 0x1B 0x36 0x1B 0x36
	0x1B 0x36 0x1B 0x36 0x9B 0x36 0x99 0xB6 0xD9 0xF6 0xF8 0xE6 0xF0 0x06 0x00 0x00
	0x00 0x00 0xCF 0x3F 0xDF 0xBF 0x19 0x8C 0x9F 0x8C 0x1F 0x8C 0xD9 0x8C 0xD9 0x8C
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x80 0x3E 0xC7 0xBF 0xCF 0xB7 0x6C 0x31 0x6C 0x31 0x6C 0x31
	0x6E 0x31 0xCE 0x33 0xCC 0x33 0xCC 0x36 0x8C 0x3E 0x0C 0x3C 0x0C 0x36 0x0C 0x36
	0x0C 0x36 0x0C 0x33 0x0C 0x33 0x0C 0x31 0x0C 0x31 0x0F 0xB1 0x07 0xB0 0x00 0x00
	0x00 0x00 0x7C 0x00 0x7E 0x00 0x66 0x00 0x7C 0x00 0x66 0x00 0x7E 0x00 0x7C 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x80 0x00 0x80 0x00 0x80 0x00
	0x80 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x80 0x00 0x80 0x00 0x80 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0xCC 0x00 0xCC 0x00 0xFC 0x00 0x78 0x00 0x30 0x00 0x30 0x00 0x30 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

: victory-data
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x03 0x00 0x0C 0x00 0x30 0x00 0x40 0x01 0x80 0x02 0x0F 0x04 0x30 0x08 0x80
	0x11 0x00 0x10 0x00 0x22 0x00 0x20 0x00 0x20 0x00 0x23 0xF8 0x2D 0x84 0x32 0x02
	0x34 0x02 0x14 0x02 0x1D 0x06 0x06 0x18 0x00 0x60 0x01 0x80 0x06 0x03 0x08 0x0C
	0x10 0x10 0x10 0x00 0x20 0x40 0x20 0x80 0x20 0x80 0x20 0x80 0x11 0x00 0x11 0x00
	0x10 0x00 0x10 0x81 0x08 0x06 0x04 0x0A 0x02 0x14 0x01 0x93 0x00 0x7F 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0xF8 0x38 0x07
	0xC0 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x80 0x00 0x78 0xC3 0x00 0xC3
	0x00 0xC3 0x00 0x63 0x00 0x63 0x00 0x67 0x00 0x66 0x00 0x36 0x00 0x36 0x00 0x36
	0x00 0x36 0x00 0x36 0x00 0x1E 0x00 0x1C 0x3C 0x0C 0xC0 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x08 0x00 0x00 0x00 0x08 0x00 0x00 0x00 0x00 0x1E 0x00
	0x61 0xC0 0x80 0x20 0x00 0x21 0x00 0x22 0x40 0x44 0x40 0x84 0x81 0x08 0x02 0x00
	0x02 0x08 0x02 0x00 0x03 0x00 0x00 0xF8 0x00 0x07 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x0F 0x00 0x30 0x00 0x40 0x00 0x80 0x00 0xC0
	0xC0 0x40 0x38 0xC0 0x07 0x80 0x00 0x00 0x00 0x00 0x07 0x00 0x0F 0x9E 0x6D 0xDE
	0x6C 0xCC 0x6C 0xCC 0x6C 0xCC 0x6C 0x0C 0x6C 0x0C 0x6C 0x0C 0x6C 0x0C 0x6C 0x0C
	0x6C 0xCC 0x6C 0xCC 0x6C 0xCC 0x6F 0xCC 0x67 0x8C 0x60 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x80 0x00 0x00 0x00 0x00 0x00 0x00
	0x40 0x00 0x80 0x0F 0x00 0x30 0x00 0x40 0x00 0x80 0x01 0x01 0x02 0x01 0x02 0x81
	0x06 0x42 0x05 0xC2 0x04 0x42 0x02 0x82 0xFF 0x01 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x7F 0xC0 0x98 0x3C 0x20 0x07 0x40 0x38 0x00 0x40 0x00 0x47
	0x00 0xB8 0x00 0xC0 0x00 0x80 0x00 0x00 0x00 0x00 0x39 0xE6 0x7D 0xF6 0xED 0xBB
	0xCD 0x9B 0xCD 0x99 0xCD 0x99 0xCD 0x99 0xCD 0xB9 0xCD 0xF1 0xCD 0xE1 0xCD 0xF1
	0xCD 0xB1 0xCD 0x99 0xED 0x99 0xFD 0x8D 0x79 0x8C 0x01 0x80 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x00
	0x00 0x00 0x81 0x00 0x42 0x00 0x44 0x00 0x84 0x03 0x08 0x0C 0x08 0x10 0x10 0x21
	0x10 0x42 0x10 0x44 0x00 0x88 0x00 0xA8 0xC0 0xEC 0x3F 0xC3 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x80 0x00 0x00 0x00 0xF8 0x00
	0x07 0xC0 0x00 0x20 0x00 0x18 0x00 0x04 0x00 0x02 0x1B 0x3F 0x3B 0x41 0x33 0x3C
	0x63 0x02 0xE7 0x01 0xC6 0x00 0xC6 0x00 0x86 0x00 0x86 0x1E 0x8C 0x01 0x8C 0x00
	0x8C 0x00 0x8C 0x00 0x80 0x00 0x8C 0x00 0x8C 0x00 0x00 0x01 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x01 0x00 0x00 0x40 0x00 0x80 0x00 0x40 0x01 0x00 0x02 0x00 0x03
	0x00 0x00 0x02 0x00 0x01 0x80 0x00 0x40 0x00 0x20 0x80 0x10 0x80 0x08 0x00 0x04
	0x00 0x02 0x00 0x02 0x00 0x01 0x00 0x01 0x00 0x00 0xF8 0x00 0x07 0x00 0x00 0xE0
	0x00 0x1C 0x00 0x02 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x80 0x00 0x40 0x00 0x20 0x00 0x10 0x00 0x10 0x00 0xC8 0x00 0x24 0x00
	0x04 0x00 0x02 0x00 0x02 0x00 0x01 0x00 0x3D 0x00 0xC7 0x00 0x61 0x00 0x18 0x00
	0x04 0x00 0x82 0x00 0x62 0x00 0x09 0x00 0x01 0x00 0xF1 0x00 0x09 0x00 0x05 0x00
	0x87 0x00 0x4B 0x00 0x26 0x00 0x10 0x00 0x10 0x00 0x10 0x00 0x08 0x00 0x08 0x00
	0x08 0x00 0x08 0x00 0x08 0x00 0x08 0x00 0x08 0x00 0x90 0x00 0x90 0x00 0x10 0x00
	0x50 0x00 0x10 0x00 0x20 0x00 0xA0 0x00 0xA0 0x00 0x40 0x00 0x40 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

to-code

# note: this routine expects i and plane to be
# configured properly before a call:
: screen-blit
	v0 := 0
	v1 := 0
	v2 := 16
	v3 := 32
	v4 := 48
	loop
		sprite v0 v1 0  i += v3
		sprite v0 v2 0  i += v3
		sprite v0 v3 0  i += v3
		sprite v0 v4 0  i += v3
		v0 += 16
		if v0 != 128 then
	again
;

###########################################
#
#  Sound Effects
#
###########################################

to-data

# sound effects are stored as a 1-byte duration (1/60s of a second) followed by
# 16 bytes of 4khz 1-bit waveform data.

: sound-jump 1  0xE3 0x8E 0x38 0xE3 0x8E 0x38 0xC7 0x1C 0x71 0xC7 0x1C 0x71 0xCE 0x38 0xE3 0x8E
: sound-step 1  0x02 0x21 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
: sound-land 1  0xA5 0xDF 0x71 0xDC 0x50 0x1D 0x38 0x97 0x5A 0x7D 0xDF 0xB5 0xE5 0xF4 0x97 0xD5
: sound-hurt 4  0x02 0x21 0x00 0x08 0x66 0x00 0xD8 0x01 0xB0 0x20 0x10 0x10 0x00 0x00 0x00 0x00
: sound-get  4  0xF8 0xF8 0xF8 0xF8 0xF8 0xF8 0xF8 0xF8 0xF8 0xF8 0xF8 0xF8 0xF8 0xF8 0xF8 0xF8

to-code

# this is the simplest way to use the XO-Chip audio pattern buffer:
# brief, percussive patterns played in a set-and-forget manner.
# see Octo's Audio Editor in the toolbox to preview and edit these sounds.

: play-sound
	load stemp - stemp
	vf := 1
	i += vf
	audio
	buzzer := stemp
;

:macro sfx SOUND {
	i := long SOUND
	play-sound
}

###########################################
#
#  The Game Board
#
###########################################

# these macros provide shorthands for building maps as well
# as longer named constants for game logic for each tile:
:macro tile-def NAME SYMBOL {
	:calc  NAME   { 8 * CALLS }
	:macro SYMBOL { :byte NAME }
}
:macro special-def NAME SYMBOL {
	:calc  NAME   { - 1 + CALLS }
	:macro SYMBOL { :byte NAME }
}

to-data

# tiles are 8x4, in 2-bit color.
# the second plane's color is exclusively used
# for drawing the player and "dangerous" objects,
# allowing us to use the vf register to probe for
# pixel-perfect overlaps between dangerous objects
# and the player.
: tiles
	0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00  tile-def TILE_NONE      .
	0x7E 0x42 0x7E 0x42 0x00 0x00 0x00 0x00  tile-def TILE_LADDER    H
	0x00 0x00 0x00 0x00 0x00 0x22 0x66 0x66  tile-def TILE_SPIKE     ^
	0x00 0x00 0x00 0x00 0x66 0x66 0x22 0x00  tile-def TILE_DOWNSPIKE v
	0x7E 0x24 0x00 0x00 0x00 0x00 0x00 0x00  tile-def TILE_ONEWAY    $
	0xFF 0xFF 0xFF 0xFF 0x00 0x00 0x00 0x00  tile-def TILE_SOLID     @
	0xFF 0xF7 0x62 0x40 0x00 0x00 0x00 0x00  tile-def TILE_STELAC    V
	0xFF 0x00 0xFF 0xFF 0x00 0x00 0x00 0x00  tile-def TILE_TOP       -
	0x7F 0x80 0xBF 0xFF 0x00 0x00 0x00 0x00  tile-def TILE_TOP_L     /
	0xFE 0x01 0xFD 0xFF 0x00 0x00 0x00 0x00  tile-def TILE_TOP_R     \
	0xFF 0x7E 0x81 0x00 0x00 0x00 0x00 0x00  tile-def TILE_PLAT      x
	0x77 0x55 0x77 0x00 0x00 0x00 0x00 0x00  tile-def TILE_BOX       o
	0x7F 0x41 0x41 0x41 0x00 0x00 0x00 0x00  tile-def TILE_BOX_TOP   k
	0x41 0x41 0x7F 0x00 0x00 0x00 0x00 0x00  tile-def TILE_BOX_BOT   l

special-def TILE_PLAYER_L A
special-def TILE_PLAYER_R B
special-def TILE_PLAYER_D C
special-def TILE_PLAYER_U D
special-def TILE_BAT      b
special-def TILE_LEAF     L

# 16x16 tiles in 128x64 pixel hi-res mode
: board
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

# rooms are copied to the 'board' buffer before
# drawing/gameplay, so that the board may be
# modified in-place and later restored.
# it also gives us an opportunity to normalize
# "special" tiles into empty space and
# reduces indirection during gameplay.

: room-0
	V @ V . . . . . . . . . . . V V
	. V . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . C . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	A . . . . . . . . . . . . . . B
	. . . . . . . . . . . . . . . .
	- - - - - - - - - - - - \ . / -
	@ @ @ @ @ @ @ @ @ @ @ @ @ . @ @
	@ @ @ @ @ @ @ @ @ @ @ @ @ . @ @
: room-1
	. . . . . . . . . . . . . . . @
	. . . . . . . . . . . . . . . @
	. . . . . . . . . . . C . . . @
	. . . . . . . . . . . . . . . @
	. . . . . . . . . . . $ . . . @
	. . . . . . . . . . . . . . . @
	. . . . . . . . . . . . . . . @
	. . . . . . . . . . . . . . . @
	. . . . . . . . . . . . . . . @
	. . . . . . . . . . . . $ $ . @
	. . . . . . . . . . . . . . . @
	A . . . . . . . . D . . . . . @
	. . . / \ . . . . . . . . . . @
	- - . @ @ - - - - \ . . . . ^ @
	@ @ ^ @ @ @ @ @ @ @ . . . . @ @
	@ @ @ @ @ @ @ @ @ @ . . . . . @
: room-2
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	A . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	- - - \ . . . . . . . . . . . .
	@ @ @ @ . . . . . . . . . . . .
	@ @ @ @ . . . . . . . . . . . .
	@ @ @ @ - - - \ . . . . . . . B
	@ @ @ @ @ @ @ @ . . . . . . . .
	@ @ @ @ @ @ @ @ - - - - - - - -
	@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @
	@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @
: room-3
	@ @ @ @ @ @ @ @ @ @ @ @ @ C @ @
	@ @ @ @ @ @ @ @ @ @ @ @ V . V @
	. . . V V . . . V . . V . . . V
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . b . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	A . . . . . . . . . . . . . . B
	. . . . . . . . . . . . . . . .
	o o o o . k k o o . . . . . k k
	o k o o o l l o o o k o o o l l
	o l o o o o o o o o l o o o o o
	o o o k o o o o o o o o k o o o
: room-4
	@ @ @ @ @ @ @ @ @ @ . . . C . @
	@ @ @ @ @ @ @ @ @ @ . . . . . @
	. . . V V . V @ @ V . . . . . @
	. . . . . . . V V . . . . . . @
	. . . . . . . . . . . . . . . @
	. . . . . . . . L . . . . . . @
	. . . . . . . . . . . . . ^ ^ @
	. . . . . . . . o o . . . @ @ @
	. . o . . . . . k o . . . V @ @
	. . . . . . . . l k . . . . . .
	A . . . . . . . o l . . . . . B
	. . . . . . . . o o . . . . . .
	k k o o o . . . o o . . . k k k
	l l o k k . . . k o . . . l l l
	. . o l l . . . l o . . . k k k
	. . o o o . . . . . . . . l l l
: room-5
	@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @
	@ @ @ @ V V . . . . . . V @ @ @
	@ @ @ V . . . . . . L . . @ @ o
	@ o @ . . . . . . . . . . @ o o
	@ o @ . . . / - - - \ . . @ @ @
	@ @ @ . . . @ @ @ @ @ . . @ @ @
	@ @ o . . / @ @ @ @ @ . . @ @ @
	o o o . . @ . . v v v . . o @ @
	o o o $ $ V . . . . . . . o o o
	. . . . . . . . . . . . . o o o
	A . . . . . . . . . . . . k k k
	. . . . . . . . . . . . . l l l
	k k k k k . . k k k k . . k k k
	l l l l l ^ ^ l l l l ^ ^ l l l
	. . k k k k k k k k k k k k k k
	. . l l l l l l l l l l l l l l
: room-6
	o o o o o . . . . . . . . o l l
	o o o k k . . . . . . . C o o l
	o k k l l . . . . . . . . o k k
	o l l o o . . . . . . . . o l l
	k k o . o . . . . . . . . . o o
	l l . . . . . . . . . . . . k k
	k k . . . . . . . . . . . . l l
	l l . . . . . . . . . . . . k k
	k . . . . . . . . . . . . . l l
	l . . . . . . . . . . . . . k k
	A . . . . . . . . . . . . . l l
	. . . . . . . . . . . . o o o o
	k k . . . . k k . . . . k k k k
	l l ^ ^ ^ ^ l l ^ ^ ^ ^ l l l l
	k k k k k k k k k k k k k k k k
	l l l l l l l l l l l l l l l l
: room-7
	k k k k k k k k o o o o o o k k
	l l l l l l l l v v v v v v l l
	A . . . . . . . . . . . L . k k
	. . . . . . . . . . . . . . l l
	o o o o o o o o . . o o . o k k
	o o v v v v v v . . v v . v l l
	k k . . . . . . . . . . . . k k
	l l . . . . . . . . . . . . l l
	k k o . . o o o o o o o o o k k
	l l v . . v v v v v v v v v l l
	k k . . . . . . . . . . . . . B
	l l . . . . . . . . . . . . . .
	k k k k k k k k k k k k k k k k
	l l l l l l l l l l l l l l l l
	. . k k k k k k k k k k k k k k
	. . l l l l l l l l l l l l l l
: room-8
	k k k k k k k k k k k . . k k k
	l l l l l l l l l l l . . l l l
	k k . . . . . . . . . . . . . B
	l l . . . . . . . . . . . . . .
	k k . . . . . . . . k k k k k k
	l l . . . . . . . . l l l l l l
	o o . . . . . . . . k o o o o o
	o o ^ ^ . ^ ^ . $ . l o o o o o
	o o o o L o o . . . k o o o o o
	o o o o . o o . . . l o o o o o
	o o . o . o . . . . k o o o o o
	o o . . . . . . $ . l o o o o o
	o k . . . . . . . . k o o o o o
	o l . . . . . . . . l o o o o o
	@ @ ^ ^ ^ . . . o o o o o o o o
	@ @ o o o o o o o o o o o o o o
: room-9
	@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @
	@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @
	. . . . . V . . . . . . V . . .
	. . . . . . . . . . . . . . . .
	. . . L . . . . . . . . . . . .
	. . . . . . . . . . . . . . . b
	. . . $ . . . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	. . . . . $ . . . . . . . . . .
	. . . . . . . . . . . . . . . .
	A . . . . . . . . . . . . . . B
	. . . . . . . . . . o $ $ o . .
	o k o . . . o o o k o . D o o o
	o l o ^ ^ ^ o o o l o . . o o o
	@ @ o o k o k o o o o $ $ o o o
	@ @ o o l o l o o o o . . o o o
: room-10
	@ @ @ @ @ @ @ @ @ @ @ . . @ @ @
	@ @ @ @ @ @ @ @ @ @ @ . . V @ @
	@ @ V . . . . . . . V . . . V .
	k V . . . . . . . . . $ $ . . .
	l . . . . . . . . . . . . . . .
	o . . . . / - - - \ . . . . . .
	k . L . . @ @ @ @ @ . $ $ . . .
	l . . . . @ V . . . . . . . . b
	k . . . . @ . . . . . . . . . .
	l . . . . . . . . . . $ $ . . .
	o ^ . . . . . . . . . . . . . B
	k o ^ . . . . . . . . . . . . .
	l k k ^ o o o o o o o o o o o k
	o l l o o o o o o o o o k o k l
	@ @ o o o o o o k o o o l o l o
	@ @ o o o o o o l o o o o o o o
: room-11
	@ @ V . . . . . V @ V V . . . .
	@ V . . . . . . . V . . . . . .
	@ . . . . . . . . . . . . . . b
	@ . . . . . . . . . . . . . . .
	@ . . . . . . . . . . . . . . .
	@ . . . . . . . . . . . . . . .
	@ . . . . . . . . . . / \ . . B
	@ . . . . . . / \ . / @ @ . . .
	@ . . . / - - @ @ - @ @ @ - - -
	@ . . . @ @ @ @ @ @ @ @ @ @ @ @
	@ . . . . . . . . . . . V @ @ @
	@ . . . . . . . . . . . . @ @ @
	@ \ . . . . . . . . . . . @ @ @
	@ @ - - - - - - - - \ . D @ @ @
	@ @ @ @ @ @ @ @ @ @ @ . . @ @ @
	@ @ @ @ @ @ @ @ @ @ @ $ $ @ @ @
: room-12
	. . . . . . . . . . . V @ @ @ @
	. . . . . . . . . . . . V V @ @
	. . . . . . . . . . . . . . V @
	. . . . . . . . . . . . . . . @
	. . . . . . . . . L . . . . . @
	. . . . . . . . . . . . . . . @
	\ . . . . . . . / \ . . . . . @
	@ . $ $ . . . . @ @ . . . . . @
	V . . . . . . . v v . . . . . @
	. . . . . . . . . . . . . . . @
	. . . . . . . . . . . . . . . @
	. . . . . . . . . . . . . . . @
	. . . . . $ $ . . . . . . . . @
	. . . . . . . . . . . D . . . @
	. . . . . . . . . . . . . . . @
	. . . . . . . . . . . $ . . . @
: room-13
	@ @ V . . . . . . . . . . . . .
	@ @ . . . . . . . . . . . . . .
	@ @ . . . . . . . . . . . . . .
	@ @ . . . . . . . . . . . . . .
	@ @ . . . . . . . . . . . . . B
	@ @ . . . . . . . . . . . . . .
	@ @ . . . . . . . . . . / - - -
	@ V . . L . . . . . . . @ @ @ @
	@ . . . . . . . . . . . V V @ V
	@ . . . $ . . . $ . . . . . V .
	@ . . . . . . . . . . . . . . .
	@ . . . . . . . . . . . . . . .
	@ ^ . . . ^ ^ ^ . . . . . . . .
	@ @ . . . V @ V . . . . . . . .
	@ @ ^ . . . V . . . . . . . . ^
	@ @ @ . . . . . . . . . . . ^ @

: rooms
	pointer room-0
	pointer room-1
	pointer room-2
	pointer room-3
	pointer room-4
	pointer room-5
	pointer room-6
	pointer room-7
	pointer room-8
	pointer room-9
	pointer room-10
	pointer room-11
	pointer room-12
	pointer room-13

# for reference, here's how all the current rooms
# are laid out spatially:
#
#        13 12
#   11 2  0  1
#   10 9  3  4  5
#      8  7  6
#

: adjacency
	# the index of the room in direction { LEFT, RIGHT, UP, DOWN }
	# or -1 to indicate no room in that direction:
	 2   1  13   3 # 0  (start)
	 0  -1  12   4 # 1
	11   0  -1  -1 # 2
	 9   4   0  -1 # 3
	 3   5   1   6 # 4  (leaf)
	 4  -1  -1  -1 # 5  (leaf)
	 7  -1   4  -1 # 6
	 8   6  -1  -1 # 7  (leaf)
	-1   7   9  -1 # 8  (leaf)
	10   3  -1   8 # 9  (leaf)
	-1   9  11  -1 # 10 (leaf)
	-1   2  -1  10 # 11
	13  -1  -1   1 # 12 (leaf)
	-1  12  -1   0 # 13 (leaf)

to-code

# do this first, in its own pass, so that
# we can hide the time it takes and speed up drawing:
: board-unpack
	bat-clear
	leaf-clear
	i := player-room
	load v0
	i := long rooms
	i += v0
	i += v0
	load v1
	i := room-source
	save v1

	v1 := 0 # x
	v2 := 0 # y
	v3 := 0 # index
	loop
		indirect room-source
		i += v3
		load v0
		:macro player-spawner TILE {
			if v0 == TILE begin
				:calc dest { room-spawns + CALLS * 2 }
				i := dest
				save v1 - v2
				v0 := TILE_NONE
			end
		}
		player-spawner TILE_PLAYER_R
		player-spawner TILE_PLAYER_L
		player-spawner TILE_PLAYER_U
		player-spawner TILE_PLAYER_D
		if v0 == TILE_BAT begin
			bat-init
			v0 := TILE_NONE
		end
		if v0 == TILE_LEAF begin
			leaf-init
			v0 := TILE_NONE
		end

		i := long board
		i += v3
		save v0

		v1 += 8
		if v1 == 128 then v2 += 4
		if v1 == 128 then v1 := 0
		v3 += 1
		if v3 != 0 then
	again
;

# in principle this routine could be unrolled
# for speed, but it is not observably the limiting
# factor in the game engine:
: board-draw
	plane 3
	v1 := 0 # x
	v2 := 0 # y
	v3 := 0 # index
	loop
		i := long board
		i += v3
		load v0
		i := long tiles
		i += v0
		sprite v1 v2 4

		v1 += 8
		if v1 == 128 then v2 += 4
		if v1 == 128 then v1 := 0
		v3 += 1
		if v3 != 0 then
	again
	bat-draw
	leaf-draw

	i := player-room
	load v0
	if v0 == 0 begin
		plane 1
		i := long title-data
		screen-blit
	end
;

###########################################
#
#  The Player
#
###########################################

:const PLAYER_HEIGHT 8
:calc  FACE_LEFT { PLAYER_HEIGHT * 7 }
:macro frame-def NAME {
	:calc NAME { PLAYER_HEIGHT * CALLS }
}

to-data

: player-frames
	0x00 0x3C 0x28 0x3C 0x7E 0x7E 0x3C 0x24  frame-def FRAME_STAND
	0x00 0x3C 0x28 0x3C 0xFC 0x3C 0x3C 0x24  frame-def FRAME_RUN_0
	0x3C 0x28 0x3C 0xFC 0x3C 0x3C 0x28 0x08  frame-def FRAME_RUN_1
	0x00 0x3C 0x28 0x3C 0xFC 0x3C 0x3C 0x18  frame-def FRAME_RUN_2
	0x3C 0x28 0x3C 0xFC 0x3C 0x3C 0x18 0x10  frame-def FRAME_RUN_3
	0x3C 0x28 0x3C 0x3C 0x7E 0x3C 0x28 0x00  frame-def FRAME_JUMP
	0x00 0x3C 0xA9 0x7E 0x3C 0x3C 0x3C 0x24  frame-def FRAME_FALL
: player-frames-mirrored
	mirror-frame FRAME_STAND
	mirror-frame FRAME_RUN_0
	mirror-frame FRAME_RUN_1
	mirror-frame FRAME_RUN_2
	mirror-frame FRAME_RUN_3
	mirror-frame FRAME_JUMP
	mirror-frame FRAME_FALL

: player-dead
	0x00 0x3C 0x89 0x7E 0x3C 0x3C 0x3C 0x42
: dust-animation
	0x00 0x00 0x00 0x00 0x00 0x00 0x18 0x00
	0x00 0x00 0x00 0x00 0x00 0x38 0x66 0x18
	0x00 0x00 0x00 0x00 0x00 0x38 0x66 0x18
	0x00 0x00 0x00 0x00 0x00 0x00 0x18 0x00
:calc DUST_END { 4 * 8 }

# the player's vertical trajectory following a jump is controlled
# through this lookup table, which is a series of y deltas.
# when the player steps off a ledge, they immediately skip to
# the downward part of the arc. Upon reaching the end of the table,
# the player will maintain downward motion at TERMINAL_VELOCITY
# until they collide with a floor surface:
: player-arc      -3 -3 -3 -2 -2 -1 -1 -1 0
: player-arc-down 1 1 1 2 2 3
:const TERMINAL_VELOCITY 3
:byte  TERMINAL_VELOCITY
:calc  ARC_LENGTH { -1 + HERE       - player-arc }
:calc  ARC_TOP    { player-arc-down - player-arc }

# when a player steps off a ledge, they have a few frames during
# which they are still permitted to initiate a jump, known as
# "coyote time" in reference to Looney Tunes:
:calc  COYOTE_GRACE_PERIOD { ARC_TOP + 2 }

to-code

: player-spawn
	px_sign := 0
	dx      := 0
	flags   := 0
	frame   := 0

	i := player-entrance
	load v0
	if v0 == ENTER_RIGHT begin
		set-flag FLAG_LEFT
	end
	i := room-spawns
	i += v0
	i += v0
	load px - py
;

: player-init
	# like player-draw, but for the first frame only; no erasing!
	plane 2
	i := long player-frames
	sprite px py PLAYER_HEIGHT

	# initialize the previous frame buffer:
	i  := player-prev
	v0 := 0
	v1 := px
	v2 := py
	save v2

	i  := player-dust
	v0 := DUST_END
	save v0
;

:macro player-poll-release {
	if-flag FLAG_PRESSED begin
		vf := OCTO_KEY_E
		if vf -key begin
			clear-flag FLAG_PRESSED
		end
	end
}

# The player-test routines are the basis for all
# player-map collision. Since this happens very frequently,
# these two chunks of the board indexing process have been
# factored into subroutines:
: player-test-a
	v1 += px
	clamp v1 -16 1  128 127
	v1 >>= v1 # / 2
	v1 >>= v1 # / 4
	v1 >>= v1 # / 8
	i := long board
	i += v1
;
: player-test-b
	v1 += py # /4, *16 ...
	clamp v1 -16 1  64 63
	v2 := 0b11111100
	v1 &= v2
	v1 <<= v1
	v1 <<= v1
	i += v1
	load v1 - v1
;
# probe the map at an offset from the player location (in pixels),
# and return the TILE_XXX value at that location:
:macro player-test XOFF YOFF {
	v1 := XOFF
	player-test-a
	v1 := YOFF
	player-test-b
}
:macro solid-test XOFF YOFF { # OR a result into v4
	player-test XOFF YOFF
	if v1 >= TILE_SOLID then v4 := 1
}
:macro floor-test XOFF YOFF { # OR a result into v4
	player-test XOFF YOFF
	if v1 >= TILE_ONEWAY then v4 := 1
}

# the player bounding box proper is factored out into macros here
# so that it can be viewed/edited together more easily.
# users of these routines must clear v4 before invocation:
:macro bounding-box-l {
	solid-test  0 0
	solid-test  0 7
}
:macro bounding-box-r {
	solid-test  7 0
	solid-test  7 7
}
:macro bounding-box-u {
	solid-test  2 -1
	solid-test  6 -1
}
:macro bounding-box-d {
	# the special treatment of the "down" boundaries
	# is for "one-way" platforms, which are only
	# solid when the player is moving downward:
	floor-test  2 PLAYER_HEIGHT
	floor-test  6 PLAYER_HEIGHT
}
:macro bounding-box-eject {
	# additionally, if we slip into one-way platforms
	# horizontally, we may need to push the player
	# up to the top of the platform:
	v0 := 0
	player-test 2 7
	if v1 == TILE_ONEWAY then v0 := 1
	player-test 6 7
	if v1 == TILE_ONEWAY then v0 := 1
	if v0 != 0 then py += -1
}

:macro player-horizontal {
	:macro horizontal-movement KEY DELTA LEFT BOUNDS {
		vf := KEY if vf key begin
			dx := 4
			if-flag FLAG_JUMPED then dx := 2
			px_sign := DELTA
			LEFT FLAG_LEFT
		end
		if px_sign == DELTA begin
			# always probe the bounding box while
			# adding a single pixel to position at a time;
			# this ensures we don't penetrate walls:
			v0 := 0
			loop
				v4 := 0
				BOUNDS
				if v4 != 0 begin
					px_sign := 0
					dx := 0
					v0 := dx
				end
				while v0 != dx
				v0 += 1
				px += DELTA
			again
		end
	}
	horizontal-movement OCTO_KEY_A -1 set-flag   bounding-box-l
	horizontal-movement OCTO_KEY_D  1 clear-flag bounding-box-r
	if-flag FLAG_LEFT then v3 += FACE_LEFT
}

:macro player-do-jump {
	:calc JUMP_AND_PRESSED { FLAG_JUMPED | FLAG_PRESSED }
	set-flag JUMP_AND_PRESSED
	frame := 0
	py += -1

	i  := player-dust
	v0 := 0
	v1 := px
	v2 := py
	save v2

	sfx sound-jump
}

:macro player-jumping {
	# jump in a grace period after ledge-falling?
	vf := OCTO_KEY_E if vf key begin
		if frame <= COYOTE_GRACE_PERIOD begin
			if-flag FLAG_FELL begin
				if-not-flag FLAG_PRESSED begin
					clear-flag FLAG_FELL
					player-do-jump
				end
			end
		end
	end
	i := long player-arc
	i += frame
	load v0
	vf := 0b1000000
	vf &= v0
	if vf != 0 begin
		# moving up; collide with roof?
		# as before, probe iteratively and move in
		# single-pixel steps:
		loop
			v4 := 0
			bounding-box-u
			if v4 != 0 begin
				# hit ceiling!
				frame := ARC_TOP
				v0 := 0
			end
			while v0 != 0
			py += -1
			v0 += 1
		again
		v3 += FRAME_JUMP
	else
		if v0 != 0 begin
			# moving down; collide with floor?
			loop
				v4 := 0
				bounding-box-d
				if v4 != 0 begin
					# hit the ground?
					:calc GROUND_RESET { FLAG_JUMPED | FLAG_FELL }
					clear-flag GROUND_RESET
					v0 := 0
					sfx sound-land
				end
				while v0 != 0
				py += 1
				v0 += -1
			again
			v3 += FRAME_FALL
		end
	end
	if frame != ARC_LENGTH then frame += 1
}

:macro player-walking {
	# allow the player to jump
	vf := OCTO_KEY_E if vf key begin
		if-not-flag FLAG_PRESSED begin
			player-do-jump
		else
			jump normal-walking
		end
	else
		: normal-walking
		# friction applies while on the ground
		if dx != 0 then dx += -1
		if dx == 0 then px_sign := 0

		# ground animation
		if px_sign == 0 begin
			# standing still
			frame := 0
			bounding-box-eject
		else
			# walking
			vf := 0b11
			vf &= frame
			if vf == 1 begin
				sfx sound-step
			end

			v3 += FRAME_RUN_0
			frame += 1
			v0 := frame
			vf := 0b110
			v0 &= vf
			v0 <<= v0 # *4
			v0 <<= v0 # *8
			v3 += v0
		end
		# fall off ledges:
		v4 := 0
		bounding-box-d
		if v4 == 0 begin
			:calc COYOTE_TIME { FLAG_JUMPED | FLAG_FELL }
			set-flag COYOTE_TIME
			frame := ARC_TOP
		end
	end
}

:macro player-die {
	# erase current player
	i := long player-frames
	i += v3
	sprite px py PLAYER_HEIGHT
	plane 1

	# erase dust, if any:
	i := player-dust
	load v2
	loop
		while v0 != DUST_END
		i := long dust-animation
		i += v0
		v0 += 8
		sprite v1 v2 8
	again

	# record the death
	i := total-deaths
	load v0 - v0
	if v0 != 99 then v0 += 1
	save v0 - v0

	# draw dead player
	sfx sound-hurt
	i := long player-dead
	sprite px py PLAYER_HEIGHT
	wait 30
	sprite px py PLAYER_HEIGHT

	# flicker on respawn
	player-spawn
	plane 2
	i := long player-frames
	if-flag FLAG_LEFT then i := long player-frames-mirrored
	v0 := 8
	loop
		sprite px py PLAYER_HEIGHT
		wait 3
		v0 += -1
		if v0 != 0 then
	again

	player-init
	jump main-body
}

:macro board-edges {
	:macro edge REGISTER MIN MAX ENTRANCE TARGET {
		if REGISTER > MIN begin
			if REGISTER < MAX begin
				i := player-room
				load v2 - v2
				:calc adjacent-room { adjacency + ENTRANCE }
				i  := long adjacent-room
				i  += v2
				i  += v2
				i  += v2
				i  += v2
				load v0
				if v0 != -1 begin
					i := player-room
					v1 := ENTRANCE
					save v1

					board-unpack
					plane 3
					clear
					board-draw

					REGISTER := TARGET
					player-init
				end
			end
		end
	}
	:const EDGE_MARGIN 16

	:calc EDGE_LEFT_MIN { 0xFF & 0 - EDGE_MARGIN }
	:calc EDGE_LEFT_MAX { 0xFF & -1 }
	edge px EDGE_LEFT_MIN EDGE_LEFT_MAX ENTER_RIGHT 120

	:calc EDGE_RIGHT_MIN { 0xFF & 128 }
	:calc EDGE_RIGHT_MAX { 0xFF & 128 + EDGE_MARGIN }
	edge px EDGE_RIGHT_MIN EDGE_RIGHT_MAX ENTER_LEFT 0

	:calc EDGE_TOP_MIN { 0xFF & 0 - EDGE_MARGIN }
	:calc EDGE_TOP_MAX { 0xFF & -1 }
	edge py EDGE_TOP_MIN EDGE_TOP_MAX ENTER_UP 56

	:calc EDGE_BOTTOM_MIN { 0xFF & 61 }
	:calc EDGE_BOTTOM_MAX { 0xFF & 61 + EDGE_MARGIN }
	edge py EDGE_BOTTOM_MIN EDGE_BOTTOM_MAX ENTER_DOWN 0
}

:macro player-update {
	# the final player frame offset will reside in v3:
	v3 := 0

	player-poll-release
	player-horizontal

	if-flag FLAG_JUMPED begin
		player-jumping
	else
		player-walking
	end

	# erase previous
	plane 2
	i := player-prev
	load v2

	# player must be clipped at edges of the
	# display for at least one frame,
	# so that room transitions will work correctly:
	if v1 < 128 begin
		if v2 < 64 begin
			i := long player-frames
			i += v0
			sprite v1 v2 PLAYER_HEIGHT
		end
	end
	# draw new frame
	if px < 128 begin
		if py < 64 begin
			i := long player-frames
			i += v3
			sprite px py PLAYER_HEIGHT

			# player overlapped the DANGER color.
			# this might be a special item (leaves, exit portal),
			# but otherwise it's a hazard that will
			# kill the player:
			if vf != 0 begin
				i := player-room
				load v0
				if v0 == 0 begin
					# the portal is the only thing on
					# the start screen with the danger color:
					portal-touch
				end
				leaf-exists
				if v0 != -1 begin
					leaf-draw
					leaf-draw
					if vf != 0 begin
						leaf-get
						jump no-consequences
					end
				end
				player-die
			end
			: no-consequences
		end
	end

	# stash the new frame's config for erasing later:
	i  := player-prev
	v0 := v3
	v1 := px
	v2 := py
	save v2

	# dust?
	plane 1
	i := player-dust
	load v2
	if v0 != DUST_END begin
		i := long dust-animation
		i += v0
		sprite v1 v2 8
		v0 += 8
		i := player-dust
		save v2
	end
}

###########################################
#
#  Title Screen / End Sequence
#
###########################################

to-data

: twinkle
	0x00 0x00 0x28 0x10 0x28 0x00 0x00 0x00
	0x00 0x10 0x10 0x6C 0x10 0x10 0x00 0x00
	0xC6 0xAA 0x6C 0x00 0x6C 0xAA 0xC6 0x00
	0x00 0x10 0x10 0x6C 0x10 0x10 0x00 0x00
	0x00 0x00 0x28 0x10 0x28 0x00 0x00 0x00

: portal
	0x00 0x00 0x00 0x10 0x00 0x00 0x00 0x00
	0x00 0x00 0x10 0x28 0x10 0x00 0x00 0x00
	0x00 0x38 0x44 0x54 0x44 0x38 0x00 0x00
	0x38 0x44 0x92 0xAA 0x92 0x44 0x38 0x00
	0x38 0x44 0x92 0xAA 0x92 0x44 0x38 0x00
	0x00 0x38 0x44 0x54 0x44 0x38 0x00 0x00
	0x00 0x00 0x10 0x28 0x10 0x00 0x00 0x00
	0x00 0x00 0x00 0x10 0x00 0x00 0x00 0x00

: twinkle-pos
	42  3
	82 25
	35 39

to-code

:macro title-sequence {
	plane 1
	v3 := 0
	loop
		v0 := 20
		loop
			vf := OCTO_KEY_E
			if vf key then jump title-sequence-drop
			vf := OCTO_KEY_D
			if vf key then jump title-sequence-drop
			vf := OCTO_KEY_A
			if vf key then jump title-sequence-drop
			sync 3
			v0 += -1
			if v0 != 0 then
		again
		i := long twinkle-pos
		i += v3
		load v1
		v2 := 0
		loop
			i := long twinkle
			i += v2
			sprite v0 v1 8
			wait 3
			sprite v0 v1 8
			v2 += 8
			:calc TWINKLE_END { 5 * 8 }
			if v2 != TWINKLE_END then
		again
		v3 += 2
		if v3 == 6 then v3 := 0
	again
	: title-sequence-drop
}

:macro portal-animate {
	i := player-room
	load v0
	if v0 == 0 begin
		i := leaf-count
		load v0
		if v0 == LEAF_COUNT begin
			plane 2
			i := long portal
			v0 := 0b1110
			v0 &= time
			v0 <<= v0
			v0 <<= v0
			i += v0
			v0 := 72
			v1 := 33
			sprite v0 v1 8
		end
	end
}

: bcd-buffer 0    # hundreds
: bcd-digits 0 0  # tens, ones

: bcd-byte # decode v0, at v2/v3...
	i := bcd-buffer
	bcd v0
	i := bcd-digits
	load v1
	i := hex v0
	sprite v2 v3 5
	v2 += 5
	i := hex v1
	sprite v2 v3 5
;

:macro portal-touch {
	sfx sound-get
	plane 3
	clear
	plane 1
	i := long victory-data
	screen-blit

	# display time and death counts:
	plane 2
	i := total-time-frac
	load v0
	v2 := 58
	v3 := 33
	bcd-byte
	i := total-time-sec
	load v0
	v2 += -17
	bcd-byte
	i := total-time-min
	load v0
	v2 += -17
	bcd-byte

	i := total-deaths
	load v0
	v2 := 83
	bcd-byte

	# wait for space, debounced:
	vf := OCTO_KEY_E
	loop if vf  key then again
	loop if vf -key then again
	loop if vf  key then again

	world-reset
	jump main
}

:const GAME_TICK_RATE 3
: increment-total-time
	v1 := 0
	loop
		vf := 1
		:macro inc-field ADDR {
			i := ADDR
			load v0 - v0
			v0 += vf
			vf := 0
			if v0 == 60 begin
				vf := 1
				v0 := 0
			end
			save v0 - v0
		}
		inc-field total-time-frac
		inc-field total-time-sec
		inc-field total-time-min

		v1 += 1
		if v1 != GAME_TICK_RATE then
	again
;

###########################################
#
#  Main Loop
#
###########################################

: main
	hires
	plane 3
	clear
	world-reset
	board-unpack
	board-draw
	player-spawn
	player-init
	title-sequence

: main-body
	time := 0
	loop
		player-update
		board-edges
		bat-update
		portal-animate
		time += 1
		increment-total-time
		vf := OCTO_KEY_1
		if vf key then jump main
		sync GAME_TICK_RATE
	again

:monitor total-deaths    1
:monitor total-time-frac 3

# changes:
# v0.1 - first public beta
# v0.2 - completed level design
# v0.3 - added timer/death counter for speedrunning
#      - added press 1 to hard-reset entire game (see above)
#      - corrected a mistake in player spawn D for room 9
